function TextModel(){
    this.init$Eventable();
    this.$chunks = [''];
    this.$charCount = 0;
    this.$splitSize = 8192;
    this.$lineTracker = new LineTracker(this, ['\r', '\n', '\r\n']);
    this.$lineTracker.update(this.getText(), 0, 0);
}
TextModel.prototype = {
    getLength: function (){
        return this.$charCount;
    },
    _indexesOf: function (startOffset, endOffset){
        var chunks = this.$chunks;
        var endChunkIndex = chunks.length;
        var length = endOffset - startOffset;
        var startIndex = 0;
        var chunkSize;
        while (startIndex < endChunkIndex) {
            chunkSize = chunks[startIndex].length;
            if (startOffset <= chunkSize) break;
            startOffset -= chunkSize;
            startIndex ++;
        }
        var endIndex = startIndex;
        endOffset = startOffset + length;
        while (endIndex < endChunkIndex) {
            chunkSize = chunks[startIndex].length;
            if (endOffset <= chunkSize) break;
            endOffset -= chunkSize;
            endIndex ++;
        }
        return [startIndex, startOffset, endIndex, endOffset];
    },
    getText: function (startOffset, endOffset){
        if (startOffset == null) {
            startOffset = 0;
        }
        if (endOffset == null) {
            endOffset = this.$charCount;
        }

        Utils.assert(startOffset >= 0 && startOffset <= endOffset);
        Utils.assert(endOffset >= 0 && endOffset <= this.$charCount);
        
        if (startOffset == 0 && endOffset == this.$charCount) {
            return this.$chunks.join('');
        }
        
        var array = this._indexesOf(startOffset, endOffset);
        return this._getText2.apply(this, array);
    },
    _getText2: function (startChunkIndex, startChunkOffset, endChunkIndex, endChunkOffset){
        var chunks = this.$chunks;
        if (startChunkIndex == endChunkIndex) {
            return chunks[startChunkIndex].slice(startChunkOffset, endChunkOffset);
        }
        var buff = chunks.slice(startChunkOffset, endChunkOffset);
        buff[0] = chunks[startChunkIndex].slice(startChunkOffset);
        buff.push(chunks[endChunkIndex].slice(0, endChunkOffset));
        return buff.join('');
    },
    setText: function (text, startOffset, endOffset){
        var oldText;
        
        if (startOffset == null) {
            startOffset = 0;
        }
        if (endOffset == null) {
            endOffset = this.$charCount;
        }

        Utils.assert(startOffset >= 0 && startOffset <= endOffset);
        Utils.assert(endOffset >= 0 && endOffset <= this.$charCount);

        if (startOffset == 0 && endOffset == this.$charCount) {
            oldText = this.$chunks.join('');
            this.$chunks = [text];
            this.$charCount = text.length;
        } else {
            var array = this._indexesOf(startOffset, endOffset);
            oldText = this._getText2.apply(this, array);
            var startChunkIndex = array[0];
            var startChunkOffset = array[1];
            var endChunkIndex = array[2];
            var endChunkOffset = array[3];
            var splitSize = this.$splitSize;
            var tidySize = splitSize >> 1;

            var chunks = this.$chunks;
            var restChunks = [chunks[startChunkIndex].slice(0, startChunkOffset),
                text, chunks[endChunkIndex].slice(endChunkOffset)];
            var lastChunk;
            if (chunks[startChunkIndex - 1]) {
                lastChunk = chunks[startChunkIndex - 1];
                startChunkIndex --;
            } else {
                lastChunk = '';
            }
            var tidyChunks = [];
            for (var i=0; i<restChunks.length; i++) {
                lastChunk += restChunks[i];
                while (lastChunk.length >= splitSize) {
                    tidyChunks.push(lastChunk.slice(0, tidySize));
                    lastChunk = lastChunk.slice(tidySize);
                }
            }
            tidyChunks.push(lastChunk);
            array = [startChunkIndex, endChunkIndex + 1 - startChunkIndex];
            array.push.apply(array, tidyChunks);
            array.splice.apply(chunks, array);
            this.$charCount += startOffset + text.length - endOffset;
        }

        this.$lineTracker.update(startOffset, oldText, text);
        this.dispatchEvent({
            type: 'Changed',
            offset: startOffset,
            deletedText: oldText,
            insertedText: text
        });
        
        return startOffset + text.length;
    },
    _setText2: function (startChunkIndex, startChunkOffset, endChunkIndex, endChunkOffset, text){
        var splitSize = this.$splitSize;
        var tidySize = splitSize >> 1;
        
        var chunks = this.$chunks;
        var restChunks = [chunks[startChunkIndex].slice(0, startChunkOffset),
            text, chunks[endChunkIndex].slice(endChunkOffset)];
        var lastChunk;
        if (chunks[startChunkIndex - 1]) {
            lastChunk = chunks[startChunkIndex - 1];
            startChunkIndex --;
        } else {
            lastChunk = '';
        }
        var tidyChunks = [];
        for (var i=0; i<restChunks.length; i++) {
            lastChunk += restChunks[i];
            while (lastChunk.length > splitSize) {
                tidyChunks.push(lastChunk.slice(0, tidySize));
                lastChunk = lastChunk.slice(tidySize);
            }
        }
        tidyChunks.push(lastChunk);
        var array = [startChunkIndex, endChunkIndex + 1 - startChunkIndex];
        array.push.apply(array, tidyChunks);
        array.splice.apply(chunks, array);
    },
    getLineCount: function (index){
        return this.$lineTracker.getLineCount(index);
    },
    getLineIndex: function (offset){
        return this.$lineTracker.getLineIndex(offset);
    },
    getLineStartOffset: function (index){
        return this.$lineTracker.getLineStartOffset(index);
    },
    getLineEndOffset: function (index, includeDelimiter){
        return this.$lineTracker.getLineEndOffset(index, includeDelimiter);
    },
    getLineDelimiter: function (index){
        return this.$lineTracker.getLineDelimiter(index);
    }
};

Utils.extend(TextModel, Eventable);